Py.Cafe

Feanor1992/

Roman Road Network

This project is dedicated to creating an interactive map that visualizes the infrastructure of the ancient Roman Empire, with a particular focus on the Roman road network, ancient ports, and key cities. The primary goal is to provide users with a powerful tool to explore and analyze historical data, allowing them to study the spatial distribution of roads, ports, and cities and obtain detailed information through interactive popups.

DocsPricing
  • AncientPorts_Europe.xlsx
  • app.py
  • requirements.txt
  • roman_roads_v2008.dbf
  • roman_roads_v2008.prj
  • roman_roads_v2008.sbn
  • roman_roads_v2008.sbx
  • roman_roads_v2008.shp
  • roman_roads_v2008.shp.xml
  • roman_roads_v2008.shx
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
import dash
from dash import dcc, html, Input, Output
import geopandas as gpd
import pandas as pd
import plotly.graph_objects as go

# Load the shapefile containing Roman roads
roads_gdf = gpd.read_file('roman_roads_v2008.shp')

# Convert coordinate reference system to EPSG:4326 (WGS 84)
roads_gdf = roads_gdf.to_crs('EPSG:4326')

# Simplify geometries to reduce the number of points and improve performance.
# First, project to a metric CRS (EPSG:3857), simplify with a tolerance in meters,
# then convert back to EPSG:4326.
roads_gdf['geometry'] = roads_gdf.geometry.to_crs("EPSG:3857") \
    .simplify(tolerance=1000, preserve_topology=True) \
    .to_crs("EPSG:4326")

# Compute an approximate boundary of the Roman Empire by taking the convex hull
# of all the Roman roads. This will be used later to filter ports.
roman_boundary = roads_gdf.unary_union.convex_hull

# Load the ancient ports CSV file
ports_df = pd.read_excel('AncientPorts_Europe.xlsx')

# Create a GeoDataFrame for ports with EPSG:4326 CRS
ports_gdf = gpd.GeoDataFrame(
    ports_df,
    geometry=gpd.points_from_xy(
        ports_df['LONGITUDE'],
        ports_df['LATITUDE']
    ),
    crs='EPSG:4326'
)

# Filter ports to only those that fall within the approximate Roman Empire boundary.
ports_gdf = ports_gdf[ports_gdf.within(roman_boundary)]

# Define colors and styles for road classification
color_dict = {
    'Major Road': 'dodgerblue',
    'Minor Road': 'silver'
}

roman_cities = pd.DataFrame({
    "City": [
        "Rome", "Carthage", "Londinium", "Constantinople", "Alexandria", "Antioch",
        "Eburacum", "Ravenna", "Mediolanum", "Massilia", "Colonia Agrippina", "Aquincum",
        "Sirmium", "Emerita Augusta", "Tarraco", "Vindobona", "Jerusalem", "Caesarea",
        "Tauromenium", "Berytus", "Narbo Martius", "Patavium", "Neapolis",
        "Lutetia", "Lugdunum", "Cartagena", "Nîmes", "Arles", "Timgad", "Leptis Magna",
        "Thessalonica", "Ephesus", "Pergamum", "Brindisium", "Augusta Treverorum",
        "Viminacium", "Dura-Europos", "Verulamium", "Cyrene", "Syracuse", "Batavodurum, later Noviomagus", "Traiectum", "Caerleon", "Deva Victrix", "Tolosa",
        "Argentoratum", "Burdigala", "Hispalis", "Olisipo", "Malaca", "Diocaesarea",
        "Tiberias", "Edessa", "Aquileia", "Mogontiacum"
    ],
    "Province": [
        "Italia", "Africa", "Britannia", "Asia", "Aegyptus", "Syria",
        "Britannia", "Italia", "Italia", "Gallia", "Germania Inferior", "Pannonia",
        "Pannonia", "Lusitania", "Hispania Tarraconensis", "Pannonia", "Palestina", "Judaea",
        "Sicilia", "Phoenice", "Gallia Narbonensis", "Italia", "Italia",
        "Gallia Lugdunensis", "Gallia Lugdunensis", "Hispania Carthaginensis", "Gallia Narbonensis",
        "Gallia Narbonensis", "Numidia", "Africa Proconsularis", "Macedonia", "Asia", "Asia",
        "Italia", "Gallia Belgica", "Moesia", "Syria", "Britannia", "Cyrenaica", "Sicilia", "Germania Inferior", "Germania Inferior", "Britannia", "Britannia", "Gallia Narbonensis",
        "Germania Superior", "Gallia Aquitania", "Hispania Baetica", "Hispania Lusitania", "Hispania Baetica", "Judaea",
        "Judaea", "Osroene", "Italia", "Germania Superior"
    ],
    "Lat": [
        41.9028, 36.8528, 51.5074, 41.0082, 31.2001, 36.2021,
        53.9590, 44.4142, 45.4642, 43.2965, 50.9375, 47.4980,
        45.0038, 38.9160, 41.1189, 48.2082, 31.7683, 32.5000,
        37.8530, 33.8938, 43.1840, 45.4064, 40.8518,
        48.8566, 45.7640, 37.6050, 43.8367, 43.6766, 35.4875, 32.6396,
        40.6401, 37.9390, 39.1233, 40.6333, 49.75, 44.0167, 34.7878,
        51.75, 32.124, 37.0755, 51.84, 52.09, 51.61, 53.19, 43.60,
        48.58, 44.84, 37.39, 38.72, 36.72, 32.78,
        32.79, 37.16, 45.83, 50.00
    ],
    "Lon": [
        12.4964, 10.3231, -0.1278, 28.9784, 29.9187, 37.1343,
        -1.0815, 12.1960, 9.1900, 5.3698, 6.9603, 19.0402,
        19.6120, -6.3430, 1.2445, 16.3738, 35.2137, 34.8920,
        15.2860, 35.5018, 3.0060, 11.8768, 14.2681,
        2.3522, 4.8357, -0.9869, 4.3601, 4.6280, 6.4689, 14.2904,
        22.9444, 27.3417, 27.1833, 17.9333, 6.6333, 21.3000, 40.9964,
        -0.3333, 21.769, 15.2866, 5.84, 5.11, -3.00, -2.93, 1.44,
        7.75, -0.57, -5.99, -9.14, -4.42, 35.25,
        35.54, 38.79, 13.44, 8.27
    ],
    "Founding": [
        "753 BC", "814 BC", "AD 47", "AD 330", "331 BC", "c. 300 BC",
        "AD 71", "c. 500 BC", "c. 600 BC", "c. 600 BC", "AD 50", "AD 89",
        "c. 300 BC", "25 BC", "218 BC", "AD 15", "c. 3000 BC", "22 BC",
        "c. 396 BC", "15 BC", "118 BC", "c. 400 BC", "c. 600 BC",
        "c. 250 BC", "43 BC", "227 BC", "470 BC", "46 BC", "AD 100", "c. 1000 BC",
        "315 BC", "c. 10th century BC", "c. 300 BC", "254 BC", "16 BC",
        "AD 29", "303 BC", "AD 50", "631 BC", "8th century BC", 
        "c. 19 BC", "AD 47", "AD 75", "AD 79", "c. 120 BC",
        "c. 12 BC", "c. 60 BC", "c. 219 BC", "c. 1200 BC", "c. 300 BC", "c. 200 BC",
        "c. 20 CE", "c. 400 BC", "181 BC", "AD 13"
    ],
    "Population": [
        "2.8 million", "Tunis: ~638,000", "9 million", "15 million", "5 million", "150,000",
        "210,000", "160,000", "1.3 million", "861,000", "1.1 million", "1.75 million",
        "35,000", "60,000", "130,000", "1.9 million", "900,000", "3,000",
        "11,000", "2.2 million", "53,000", "210,000", "3 million",
        "2.1 million", "515,000", "215,000", "150,000", "53,000", "N/A", "150,000",
        "1 million", "N/A", "70,000", "88,000", "115,000", "N/A", "N/A",
        "147,000", "40,000", "120,000", "approx 40,000", "approx 50,000", "approx 20,000", "approx 25,000", "approx 80,000",
        "approx 100,000", "approx 150,000", "approx 200,000", "approx 250,000", "approx 150,000", "approx 30,000",
        "approx 20,000", "approx 50,000", "approx 100,000", "approx 40,000"
    ],
    "Modern": [
        "Rome", "Tunis", "London", "Istanbul", "Alexandria", "Antakya",
        "York", "Ravenna", "Milan", "Marseille", "Cologne", "Budapest",
        "Sremska Mitrovica", "Mérida", "Tarragona", "Vienna", "Jerusalem", "Caesarea",
        "Taormina", "Beirut", "Narbonne", "Padua", "Naples",
        "Paris", "Lyon", "Cartagena", "Nîmes", "Arles", "Timgad", "Al Khums",
        "Thessaloniki", "Selçuk", "Bergama", "Brindisi", "Trier",
        "Archaeological site near Kostolac", "Archaeological site near Deir ez-Zor", "St Albans", "Shahhat", "Syracuse", 
        "Nijmegen", "Utrecht", "Caerleon", "Chester", "Toulouse",
        "Strasbourg", "Bordeaux", "Seville", "Lisbon", "Málaga", "Sepphoris",
        "Tiberias", "Edessa", "Aquileia", "Mainz"
    ]
})


roman_cities['Hover'] = (
    roman_cities['City'] + ' (' + roman_cities['Province'] + '), ' +
    'Founding: ' + roman_cities['Founding'] + ', ' +
    'Population: ' + roman_cities['Population'] + ', ' +
    'Modern: ' + roman_cities['Modern']
)

app = dash.Dash(__name__)

# Create dropdown options for road classes based on unique values in the original roads dataset.
road_class_options =[
    {'label': rc, 'value': rc} for rc in roads_gdf['CLASS'].unique()
]

# Set up the layout with controls and a graph.
app.layout = html.Div([
    html.H2('Roman Roads Dashboard'),
    html.Div([
        html.Label('Select Road Classes:'),
        dcc.Dropdown(
            id='road-class-dropdown',
            options=road_class_options,
            value=roads_gdf['CLASS'].unique().tolist(),  # default to all classes
            multi=True
        )
    ], style={'width': '30%', 'display': 'inline-block', 'verticalAlign': 'top'}),
    dcc.Graph(id='map-graph')
])

@app.callback(
    Output('map-graph', 'figure'),
    Input('road-class-dropdown', 'value')
)
def update_map(selected_classes):
    # Filter roads based on the selected classes.
    filtered = roads_gdf[roads_gdf['CLASS'].isin(selected_classes)].copy()
    
    # Apply fixed geometry simplification (tolerance set to 1000 meters).
    filtered['simp_geom'] = filtered.geometry.to_crs('EPSG:3857').simplify(
        tolerance=1000, preserve_topology=True
    ).to_crs('EPSG:4326')
    
    # Compute centroids and midpoints.
    filtered['centroid'] = filtered.geometry.centroid
    filtered['midpoint'] = filtered['simp_geom'].apply(lambda geom: geom.interpolate(0.5, normalized=True))
    
    # Create hover text for each road (with road length rounded to an integer).
    filtered['hover_text'] = (
        'Road Type: ' + filtered['CLASS'].astype(str) + ', ' +
        'Length: ' + filtered['Shape_Leng'].round(0).astype(int).astype(str) + ', ' +
        'Source: ' + filtered['SOURCE'].astype(str)
    )
    
    # Reassign styling parameters based on road type.
    filtered['color'] = filtered['CLASS'].map(color_dict)
    filtered['opacity'] = filtered['CLASS'].apply(lambda x: 1.0 if x == 'Major Road' else 0.7)
    filtered['width'] = filtered['CLASS'].apply(lambda x: 0.9 if x == 'Major Road' else 0.7)
    
    # Aggregate Roads by Class
    traces = []
    for road_class, group in filtered.groupby('CLASS'):
        all_lats = []
        all_lons = []
        for idx, row in group.iterrows():
            coords = list(row['simp_geom'].coords)
            all_lats.extend([pt[1] for pt in coords] + [None])
            all_lons.extend([pt[0] for pt in coords] + [None])
        style_color = group.iloc[0]['color']
        style_width = group.iloc[0]['width']
        style_opacity = group.iloc[0]['opacity']
        traces.append(go.Scattermapbox(
            lat=all_lats,
            lon=all_lons,
            mode='lines',
            line=dict(
                color=style_color,
                width=style_width
            ),
            opacity=style_opacity,
            hoverinfo='none',
            name=road_class
        ))
    
    # Add invisible markers at road midpoints for detailed hover information.
    traces.append(go.Scattermapbox(
        lat=filtered['midpoint'].apply(lambda point: point.y),
        lon=filtered['midpoint'].apply(lambda point: point.x),
        mode='markers',
        marker=dict(
            size=4,
            color='rgba(0,0,0,0)'
        ),
        text=filtered['hover_text'],
        hovertemplate='%{text}<extra></extra>',
        name='Road Details'
    ))
    
    # Add Ancient Ports Layer
    ports_trace = go.Scattermapbox(
        lat=ports_gdf.geometry.y,
        lon=ports_gdf.geometry.x,
        mode='markers',
        marker=dict(
            size=5,
            color='yellow'
        ),
        text=ports_gdf['NAME'],
        hovertemplate='Port: %{text}<extra></extra>',
        name='Ancient Ports'
    )
    traces.append(ports_trace)
    
    # Add Roman Cities Layer
    cities_trace = go.Scattermapbox(
        lat=roman_cities['Lat'],
        lon=roman_cities['Lon'],
        mode='markers',
        marker=dict(
            size=8,
            color='red'
        ),
        text=roman_cities['Hover'],
        hovertemplate='%{text}<extra></extra>',
        hoverlabel=dict(bgcolor='red'),
        name='Roman Cities'
    )
    traces.append(cities_trace)
    
    # Create and Center the Map
    fig = go.Figure(data=traces)
    if len(filtered) > 0:
        center_lat = filtered['centroid'].y.mean()
        center_lon = filtered['centroid'].x.mean()
    else:
        center_lat, center_lon = ((lat_min + lat_max) / 2, (lon_min + lon_max) / 2)
    
    fig.update_layout(
        mapbox_style='carto-darkmatter',
        mapbox_zoom=4,
        mapbox_center={
            'lat': center_lat,
            'lon': center_lon},
        height=600,
        margin=dict(
            l=0,
            r=0,
            t=0,
            b=0
        )
    )
    return fig

if __name__ == '__main__':
    app.run_server(debug=True)